using UnityEngine;
using UnityEditor;
using System;
using System.IO;

namespace Obi
{
    [CustomEditor(typeof(ObiMeshBasedActorBlueprint), true)]
    public abstract class ObiMeshBasedActorBlueprintEditor : ObiActorBlueprintEditor
    {

        [Flags]
        public enum ParticleCulling
        {
            Off = 0,
            Back = 1 << 0,
            Front = 1 << 1
        }

        protected Mesh visualizationMesh;
        protected Mesh visualizationWireMesh;
        public ParticleCulling particleCulling = ParticleCulling.Back;
       
        protected Material gradientMaterial;
        protected Material textureExportMaterial;
        protected Material paddingMaterial;

        public override void OnEnable()
        {
            base.OnEnable();
            gradientMaterial = Resources.Load<Material>("PropertyGradientMaterial");
            textureExportMaterial = Resources.Load<Material>("UVSpaceColorMaterial");
            paddingMaterial = Resources.Load<Material>("PaddingMaterial");
        }

        public abstract Mesh sourceMesh
        {
            get;
        }

        protected void NonReadableMeshWarning(Mesh mesh)
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            Texture2D icon = EditorGUIUtility.Load("icons/console.erroricon.png") as Texture2D;
            EditorGUILayout.LabelField(new GUIContent("The input mesh is not readable. Read/Write must be enabled in the mesh import settings.", icon), EditorStyles.wordWrappedMiniLabel);

            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Fix now", GUILayout.MaxWidth(100), GUILayout.MinHeight(32)))
            {
                string assetPath = AssetDatabase.GetAssetPath(mesh);
                ModelImporter modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
                if (modelImporter != null)
                {
                    modelImporter.isReadable = true;
                }
                modelImporter.SaveAndReimport();
            }
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();
        }

        protected override bool ValidateBlueprint()
        {
            if (sourceMesh != null)
            {
                if (!sourceMesh.isReadable)
                {
                    NonReadableMeshWarning(sourceMesh);
                    return false;
                }
                return true;
            }
            return false;
        }

        public abstract int VertexToParticle(int vertexIndex);

        public override void UpdateParticleVisibility(Camera cam)
        {
            if (cam != null)
            {
                for (int i = 0; i < blueprint.positions.Length; i++)
                {
                    if (blueprint.IsParticleActive(i))
                    {
                        Vector3 camToParticle = cam.transform.position - blueprint.positions[i];
                        sqrDistanceToCamera[i] = camToParticle.sqrMagnitude;

                        Vector3 normal;

                        switch (particleCulling)
                        {
                            case ParticleCulling.Off:
                                visible[i] = true;
                                break;
                            case ParticleCulling.Back:
                                normal = blueprint.restOrientations[i] * Vector3.forward;
                                visible[i] = Vector3.Dot(normal, camToParticle) > 0;
                                break;
                            case ParticleCulling.Front:
                                normal = blueprint.restOrientations[i] * Vector3.forward;
                                visible[i] = Vector3.Dot(normal, camToParticle) <= 0;
                                break;
                        }
                    }
                }

                if ((renderModeFlags & 1) != 0)
                    Refresh();
            }
        }

        public void DrawGradientMesh(float[] vertexWeights = null, float[] wireframeWeights = null)
        {
            // Due to this Unity bug: https://issuetracker.unity3d.com/issues/drawmeshnow-is-not-drawing-mesh-immediately-dx12
            // we need to create two meshes insteaf of one :(
            if (sourceMesh == null)
                return;

            visualizationMesh = GameObject.Instantiate(sourceMesh);
            visualizationWireMesh = GameObject.Instantiate(sourceMesh);

            if (gradientMaterial.SetPass(0))
            {
                var matrix = Matrix4x4.TRS(Vector3.zero, (blueprint as ObiMeshBasedActorBlueprint).rotation, (blueprint as ObiMeshBasedActorBlueprint).scale);

                Color[] colors = new Color[visualizationMesh.vertexCount];
                for (int i = 0; i < colors.Length; i++)
                {
                    int particle = VertexToParticle(i);
                    if (particle >= 0 && particle < blueprint.particleCount)
                    {
                        float weight = 1;
                        if (vertexWeights != null)
                            weight = vertexWeights[particle];

                        colors[i] = weight * currentProperty.ToColor(particle);
                    }
                    else
                        colors[i] = Color.gray;
                }

                visualizationMesh.colors = colors;
                Graphics.DrawMeshNow(visualizationMesh, matrix);

                Color wireColor = ObiEditorSettings.GetOrCreateSettings().brushWireframeColor;

                if (gradientMaterial.SetPass(1))
                {
                    for (int i = 0; i < colors.Length; i++)
                    {
                        int particle = VertexToParticle(i);
                        if (particle >= 0 && particle < blueprint.particleCount)
                        {
                            if (wireframeWeights != null)
                                colors[i] = wireColor * wireframeWeights[particle];
                            else
                                colors[i] = wireColor;
                        }
                        else
                            colors[i] = Color.gray;
                    }

                    visualizationWireMesh.colors = colors;
                    GL.wireframe = true;
                    Graphics.DrawMeshNow(visualizationWireMesh, matrix);
                    GL.wireframe = false;
                }

            }

            GameObject.DestroyImmediate(visualizationMesh);
            GameObject.DestroyImmediate(visualizationWireMesh);
        }

      
        /**
         * Reads particle data from a 2D texture. Can be used to adjust per particle mass, skin radius, etc. using 
         * a texture instead of painting it in the editor. 
         *  
         * Will call onReadProperty once for each particle, passing the particle index and the bilinearly interpolated 
         * color of the texture at its coordinate.
         *
         * Be aware that, if a particle corresponds to more than
         * one physical vertex and has multiple uv coordinates, 
         * onReadProperty will be called multiple times for that particle.
         */
        public bool ReadParticlePropertyFromTexture(Texture2D source, Action<int, Color> onReadProperty)
        {

            if (source == null || onReadProperty == null)
                return false;

            Vector2[] uvs = sourceMesh.uv;

            // Iterate over all vertices in the mesh reading back colors from the texture:
            for (int i = 0; i < sourceMesh.vertexCount; ++i)
            {
                try
                {
                    onReadProperty(VertexToParticle(i), source.GetPixelBilinear(uvs[i].x, uvs[i].y));
                }
                catch (UnityException e)
                {
                    Debug.LogException(e);
                    return false;
                }
            }

            return true;
        }

        public bool WriteParticlePropertyToTexture(string path, int width, int height, int padding)
        {

            if (path == null || textureExportMaterial == null || !textureExportMaterial.SetPass(0))
                return false;

            if (visualizationMesh == null)
            {
                visualizationMesh = GameObject.Instantiate(sourceMesh);
            }
            
            RenderTexture tempRT = RenderTexture.GetTemporary(width, height, 0);
            RenderTexture paddingRT = RenderTexture.GetTemporary(width, height, 0);

            RenderTexture old = RenderTexture.active;
            RenderTexture.active = tempRT;

            GL.PushMatrix();

            var proj = Matrix4x4.Ortho(0, 1, 0, 1, -1, 1);
            if (Camera.current != null) proj = proj * Camera.current.worldToCameraMatrix.inverse;
            GL.LoadProjectionMatrix(proj);

            Color[] colors = new Color[sourceMesh.vertexCount];
            for (int i = 0; i < colors.Length; i++)
                colors[i] = currentProperty.ToColor(VertexToParticle(i));

            visualizationMesh.colors = colors;
            Graphics.DrawMeshNow(visualizationMesh, Matrix4x4.identity);

            GL.PopMatrix();

            // Perform padding/edge dilation
            paddingMaterial.SetFloat("_Padding", padding);
            Graphics.Blit(tempRT, paddingRT, paddingMaterial);

            // Read result into our Texture2D.
            RenderTexture.active = paddingRT;
            Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
            texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);

            RenderTexture.active = old;
            RenderTexture.ReleaseTemporary(paddingRT);
            RenderTexture.ReleaseTemporary(tempRT);

            byte[] png = texture.EncodeToPNG();
            GameObject.DestroyImmediate(texture);

            try
            {
                File.WriteAllBytes(path, png);
            }
            catch (Exception e)
            {
                Debug.LogException(e);
                return false;
            }

            AssetDatabase.Refresh();

            return true;
        }
    }
}
